<?php

namespace common\behaviors;


use common\helper\Sanitizer;
use common\models\File;
use Yii;
use yii\base\Behavior;
use yii\base\Exception;
use yii\base\InvalidParamException;
use yii\base\ModelEvent;
use yii\db\ActiveRecord;
use yii\db\BaseActiveRecord;
use yii\db\Query;
use yii\helpers\FileHelper;
use yii\validators\Validator;
use yii\web\UploadedFile;

/**
 * Class FileUsageBehavior
 * @package common\behaviors
 */
class FileUsageBehavior extends Behavior
{
    public $fields = [];

    private $_files = [];

    private $_oldFileIds = [];

    public function events()
    {
        return [
            BaseActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',
            BaseActiveRecord::EVENT_AFTER_FIND => 'afterFind',
            BaseActiveRecord::EVENT_AFTER_INSERT => 'afterSave',
            BaseActiveRecord::EVENT_AFTER_UPDATE => 'afterSave',
            BaseActiveRecord::EVENT_AFTER_DELETE => 'afterDelete',
        ];
    }

    /**
     * 根据对象字段获取该字段下的文件列表
     *
     * @param string $field
     * @throws \yii\base\InvalidParamException
     * @return array
     */
    public function getFilesByField($field)
    {
        $fields = $this->getUploadFields();
        if (!isset($fields[$field])) {
            throw new InvalidParamException($field . '字段不是文件字段');
        }

        if (!isset($this->_files[$field])) {
            $fileIds = $this->getFileIdsByField($field);
            if (empty(Yii::$app->params['file.cache_enabled'])) {
                $this->_files[$field] = File::findAll(['id' => $fileIds]);
            } else {
                $this->_files[$field] = File::findFromCache($fileIds);
            }
        }

        return $this->_files[$field];
    }

    /**
     * 获取对象字段上传允许的文件类型
     *
     * @param string $field
     * @return string
     */
    public function getFileExtensionsByField($field)
    {
        $fields = $this->getUploadFields();
        if (isset($fields[$field])) {
            return $fields[$field]['rule']['extensions'];
        } else {
            return '';
        }
    }

    /**
     * 获取对象字段上传允许的最大文件大小
     *
     * @param string $field
     * @return string
     */
    public function getFileMaxSizeByField($field)
    {
        $fields = $this->getUploadFields();
        if (isset($fields[$field])) {
            return Yii::$app->formatter->asShortSize($fields[$field]['rule']['maxSize']);
        } else {
            return '';
        }
    }

    /**
     * 获取所有上传字段
     *
     * @return array
     */
    public function getUploadFields()
    {
        static $cache = [];

        /** @var ActiveRecord $owner */
        $owner = $this->owner;
        $className = get_class($owner);

        if (!isset($cache[$className])) {
            $maxSize = File::fileMaxSize();
            $rule = [
                'extensions' => File::fileExtensions(),
                'maxSize' => $maxSize,
                'minSize' => 1024,
                'checkExtensionByMimeType' => !empty(Yii::$app->params['file.check_extensions_by_mime_type']),
            ];

            $fields = [];

            foreach ((array)$this->fields as $field => $params) {
                if (!$owner->hasProperty($field, true, false)) {
                    continue;
                }

                $params = (array)$params;

                if (empty($params['many'])) {
                    $params['many'] = 1;
                } elseif (is_numeric($params['many'])) {
                    $params['many'] = abs(intval($params['many']));
                } else {
                    $params['many'] = true;
                }

                if (empty($params['type']) || !in_array($params['type'], array('file', 'image'))) {
                    $params['type'] = 'file';
                }

                if ($params['many'] === 1) {
                    $params['sort'] = false;
                } else {
                    $params['sort'] = !empty($params['sort']);
                }

                if (!isset($params['preview'])) {
                    if ($params['many'] !== 1 && $params['type'] === 'image') {
                        $params['preview'] = true;
                    } else {
                        $params['preview'] = false;
                    }
                }

                $params['isAttachment'] = !empty($params['isAttachment']);

                if (!isset($params['rule'])) {
                    $params['rule'] = $rule;
                } else {
                    $params['rule'] = array_merge($rule, $params['rule']);
                }


                if (!isset($params['rule']['tooBig'])) {
                    $params['rule']['tooBig'] = sprintf('{file}太大了，不能大于%s。', Yii::$app->formatter->asSize($params['rule']['maxSize']));
                }
                if (!isset($params['rule']['tooSmall'])) {
                    $params['rule']['tooSmall'] = sprintf('{file}太小了，不能小于%s。', Yii::$app->formatter->asSize($params['rule']['minSize']));
                }

                if ($params['type'] === 'image' && isset($params['rule']['extensions'])) {
                    $params['rule']['extensions'] = Yii::$app->get('option')->get('file.image_extensions', 'jpg,jpeg,gif,png');
                }

                if (!isset($params['attribute'])) {
                    $params['attribute'] = $field . '_id';
                }

                $params['isSelf'] = $owner->hasAttribute($params['attribute']); //是否是模型自带字段（表中有这个字段）
                $fields[$field] = $params;
            }

            $cache[$className] = $fields;
        }

        return $cache[$className];
    }


    /**
     * 上传文件到对象字段中去
     *
     * @param string $field 字段名
     * @throws \yii\base\InvalidParamException
     * @throws \yii\base\Exception
     * @throws \Exception
     * @return boolean|File
     */
    public function upload($field)
    {
        $fields = $this->getUploadFields();
        if (!isset($fields[$field])) {
            throw new InvalidParamException($field . '字段不是文件字段');
        }

        $params = $fields[$field];

        /** @var ActiveRecord $owner */
        $owner = $this->owner;

        $uploadedFile = $owner->$field = UploadedFile::getInstance($owner, $field);
        $validator = Validator::createValidator('file', $owner, $field, $params['rule']);
        $validator->validateAttribute($owner, $field);

        if ($owner->hasErrors($field)) {
            return false;
        }

        if (isset($params['location'])) {
            $location = $params['location'];
        } else {
            $location = Yii::$app->get('option')->get('file.upload_location', 'public://');
        }

        if (is_callable($location)) {
            $location = call_user_func($location);
        }


        if (isset($params['filename']) && is_callable($params['filename'])){
            $filename = call_user_func($params['filename'], $uploadedFile, $owner);
            $filename = Sanitizer::filename($filename);
            $filename = str_replace('\\', '/', $filename);
        } else {
            $filename = date('Y/m/') . '/'
                . time() . substr(md5(Yii::$app->getSecurity()->generateRandomString()), 0, 6)
                . '.' . $uploadedFile->getExtension();
        }

        if (strpos($filename, '/') !== false) {
            $location .= '/' . dirname($filename);
            $filename = basename($filename);
        }

        $directory = File::localPath($location);

        if (!$directory || !FileHelper::createDirectory($directory)) {
            throw new Exception('上传目录不能被创建');
        }

        if (substr($location, -1) == '/') {
            $uri = $location . $filename;
        } else {
            $uri = $location . '/' . $filename;
        }

        $filePath = File::localPath($uri);

        if ($uploadedFile->saveAs($filePath)) {
            $data = array(
                'user_id' => Yii::$app->getUser()->getId(),
                'bundle' => get_class($owner),
                'uri' => $uri,
                'file_name' => $uploadedFile->name,
                'file_mime' => $uploadedFile->type,
                'file_size' => $uploadedFile->size,
                'status' => $owner instanceof File ? 1 : 0,
            );

            $file = new File();
            $file->setAttributes($data, false);

            $resize = isset($params['resize']) ? $params['resize'] : true;

            if ($params['isAttachment']) {
                $resize = false;
            }

            if ($resize !== false) {
                $defaultSizes = File::imageSizes();
                if (is_array($resize)) {
                    $resize += [File::IMAGE_THUMBNAIL => $defaultSizes[File::IMAGE_THUMBNAIL]];
                } else {
                    $resize = $defaultSizes;
                }

                $file->attachBehavior('imageResize', array(
                    'class' => ImageResizeBehavior::className(),
                    'sizes' => $resize,
                ));
            } elseif ($file->isImage()) {
                if ($imageSize = getimagesize($filePath)) {
                    $file->updateMeta('sizes', [
                        File::IMAGE_ORIGIN => [
                            'width' => $imageSize[0],
                            'height' => $imageSize[1],
                            'uri' => $uri,
                        ]
                    ]);
                }
            }

            try {
                if ($file->save(false)) {
                    return $file;
                } else {
                    $file->deleteFile();
                    return false;
                }
            } catch (\Exception $e) {
                $file->deleteFile();
                throw $e;
            }
        }

        return false;
    }

    /**
     * 解析ids
     *
     * @param $ids
     * @return array
     */
    public function parseIds($ids)
    {
        if (is_string($ids)) {
            $ids = preg_split('/\s*,\s*/', trim($ids), -1, PREG_SPLIT_NO_EMPTY);
        } elseif (!is_array($ids)) {
            $ids = [];
        }

        return array_unique(array_map('intval', $ids));
    }

    /**
     * 获取对象文件ids缓存Key
     *
     * @param integer $id 对象id
     * @param string $field 对象字段名
     * @return string
     */
    public function getFileIdsCacheKey($id, $field)
    {
        $owner = $this->owner;
        $className = get_class($owner);
        return "{$className}_{$id}_{$field}_filesIds";
    }

    /**
     * 根据对象字段获取该字段下的文件ids
     *
     * @param string $field 对象字段
     * @param boolean $real 真实的已保存的
     * @throws \yii\base\InvalidParamException
     * @return array
     */
    public function getFileIdsByField($field, $real = false)
    {
        $fields = $this->getUploadFields();
        if (!isset($fields[$field])) {
            throw new InvalidParamException($field . '字段不是文件字段');
        }

        /** @var ActiveRecord $owner */
        $owner = $this->owner;

        if (isset($owner->$field) && !$real) {
            return $this->parseIds($owner->$field);
        }

        if ($owner->getIsNewRecord()) {
            return [];
        }

        if ($fields[$field]['isSelf']) {
            return $this->_oldFileIds[$field];
        }

        $cacheKey = $this->getfileIdsCacheKey($owner->getPrimaryKey(), $field);
        if (($fieldIds = Yii::$app->getCache()->get($cacheKey)) === false) {
            $query = (new Query())
                ->select('field_id')
                ->from('{{%file_usage}}')
                ->where([
                    'object_id' => $owner->getPrimaryKey(),
                    'bundle' => get_class($owner),
                    'field' => $field,
                ]);
            if ($fields[$field]['sort']) {
                $query->orderBy('weight');
            }
            if ($fields[$field]['many'] !== true) {
                $query->limit($fields[$field]['many']);
            }
            $fieldIds = $query->column($owner->getDb());
            Yii::$app->getCache()->add($cacheKey, $fieldIds,
                isset(Yii::$app->params['file.usage_ids_expire'])
                    ? Yii::$app->params['file.usage_ids_expire'] :
                    2592000);
        }

        return $fieldIds;
    }

    /**
     * 更新对象文件ids缓存
     *
     * @param integer $id 对象id
     * @param string $field 对象字段名
     * @param array $fileIds 文件ids
     */
    public function updateFileIdsCache($id, $field, $fileIds)
    {
        $cacheKey = $this->getfileIdsCacheKey($id, $field);
        Yii::$app->getCache()->set($cacheKey, $fileIds,
            isset(Yii::$app->params['file.usage_ids_expire'])
                ? Yii::$app->params['file.usage_ids_expire'] :
                2592000);
    }

    /**
     * 删除对象文件ids缓存
     *
     * @param integer $id 对象id
     * @param string $field 对象字段名
     */
    public function deleteFileIdsCache($id, $field)
    {
        $cacheKey = $this->getFileIdsCacheKey($id, $field);
        Yii::$app->getCache()->delete($cacheKey);
    }

    /**
     * @param ModelEvent $event
     */
    public function beforeValidate($event)
    {
        if (!$event->isValid) {
            return;
        }

        /** @var ActiveRecord $owner */
        $owner = $this->owner;
        if ($owner instanceof File) {
            return;
        }

        $fields = $this->getUploadFields();
        foreach ($fields as $field => $params) {
            if (!isset($owner->$field)) {
                continue;
            }

            $value = $this->parseIds($owner->$field);

            if ($params['many'] === 1) {
                $value = $value ? $value[0] : 0;
            } elseif ($params['many'] !== true) {
                $value = array_slice($value, 0, $params['many']);
            }

            $types = explode(',', $params['rule']['types']);
            $validFileIds = array();
            if (!empty(Yii::$app->params['file.cache_enabled'])) {
                $models = File::findFromCache((array)$value);
            } else {
                $models = File::findAll(['id' => $value]);
            }

            foreach ($models as $model) {
                if (in_array($model->getExtension(), $types) && $model->bundle == get_class($owner)) {
                    $validFileIds[] = $model->id;
                }
            }

            if ($params['many'] === 1) {
                $validFileIds = $validFileIds ? $validFileIds[0] : 0;
            }

            $owner->$field = $validFileIds;
        }
    }

    /**
     * @param ModelEvent $event
     */
    public function afterFind($event)
    {
        if (!$event->isValid) {
            return;
        }

        $fields = $this->getUploadFields();
        $owner = $this->owner;
        foreach ($fields as $field => $params) {
            if ($params['isSelf']) {
                $attribute = $params['attribute'];
                $this->_oldFileIds[$field] = $this->parseIds($owner->$attribute);
            }
        }
    }

    /**
     * @param ModelEvent $event
     */
    public function afterSave($event)
    {
        if (!$event->isValid) {
            return;
        }

        /** @var ActiveRecord $owner */
        $owner = $this->owner;

        //File不保存
        if ($owner instanceof File) {
            return;
        }

        $fields = $this->getUploadFields();
        foreach ($fields as $field => $params) {
            if (!isset($owner->$field)) {
                continue;
            }

            $newFileIds = (array)$owner->$field;

            $oldFileIds = $delFileIds = $addFileIds = array();
            if ($owner->getIsNewRecord()) {
                $addFileIds = $newFileIds;
            } else {
                if ($params['isSelf']) {
                    $oldFileIds = $this->_oldFileIds[$field];
                } else {
                    $oldFileIds = $this->getFileIdsByField($field, true);
                }
                $delFileIds = array_diff($oldFileIds, $newFileIds);
                $addFileIds = array_diff($newFileIds, $oldFileIds);
            }

            $connection = $owner->getDb();
            $transaction = $connection->beginTransaction();
            $objectId = $owner->getPrimaryKey();
            try {
                if ($delFileIds) {
                    $connection->createCommand()->delete('{{%file_usage}}',
                        [
                            'object_id' => $objectId,
                            'bundle' => get_class($owner),
                            'field' => $field,
                            'file_id' => $delFileIds,
                        ])->execute();

                    $boundFileIds = (new Query())
                        ->select('file_id')
                        ->from('{{%file_usage')
                        ->where(['file_id' => $delFileIds])
                        ->column($connection);

                    $realDelIds = array_diff($delFileIds, $boundFileIds);
                    foreach (File::findAll(['id' => $realDelIds]) as $file) {
                        $file->status = File::STATUS_TEMPORARY;
                        $file->delete();
                    }
                }

                if ($addFileIds) {
                    $bundle = get_class($owner);
                    $rows = [];
                    foreach ($addFileIds as $id) {
                        $rows[] = [$id, $objectId, $bundle, $field];
                    }

                    $connection->createCommand()->batchInsert('{{%file_usage}}', [
                        'file_id', 'object_id', 'bundle', 'field',
                    ], $rows)->execute();

                    File::updateAll(['status' => File::STATUS_PERMANENT], ['id' => $addFileIds]);
                }

                if ($params['sort'] && $newFileIds !== $oldFileIds) {
                    foreach ($newFileIds as $i => $id) {
                        $connection->createCommand()->update('{{%file_usage}}', ['weight' => $i], ['file_id' => $id])->execute();
                    }
                }
                $transaction->commit();
            } catch (\yii\db\Exception $e) {
                Yii::warning('保存对象文件使用信息失败。错误信息：' . $e->getMessage());
                $transaction->rollBack();
                return;
            }

            $this->updatefileIdsCache($objectId, $field, $newFileIds);
        }
    }

    /**
     * @param ModelEvent $event
     */
    public function afterDelete($event)
    {
        if (!$event->isValid) {
            return;
        }

        /** @var ActiveRecord $owner */
        $owner = $this->owner;
        $fields = $this->getUploadFields();

        if ($owner instanceof File) {
            return;
        }

        $objectId = $owner->getPrimaryKey();
        $bundle = get_class($owner);
        $connection = $owner->getDb();

        foreach ($fields as $field => $params) {
            $fileIds = $this->getFileIdsByField($field);
            $connection->createCommand()->delete('{{%file_usage}}',
                [
                    'object_id' => $objectId,
                    'bundle' => $bundle,
                    'field' => $field,
                    'file_id' => $fileIds,
                ])->execute();

            $boundFileIds = (new Query())
                ->select('file_id')
                ->from('{{%file_usage')
                ->where(['file_id' => $fileIds])
                ->column($connection);

            $realDelIds = array_diff($fileIds, $boundFileIds);
            foreach (File::findAll(['id' => $realDelIds]) as $file) {
                $file->status = File::STATUS_TEMPORARY;
                $file->delete();
            }

            $this->deletefileIdsCache($objectId, $field);
        }
    }
}